"""
Manual exploration interface implementation.

Provides interactive manual exploration of function call trees with
code viewing, navigation, and weighted node selection capabilities.
"""

import asyncio
import time
from typing import Dict, Any, List, Optional, Tuple
from pathlib import Path
from datetime import datetime

from ...core.error_handler import CodeMCPError, log_info, log_debug
from ...core.base_mcp import CallTreeNode, CallTree
from ..utils.colors import ColorPrinter, Color
from .navigation import NavigationController
from .code_viewer import CodeViewer


class ManualExplorer:
    """Interactive manual exploration interface."""
    
    def __init__(self, config: Dict[str, Any], explorer_config: Dict[str, Any]):
        self.config = config
        self.explorer_config = explorer_config
        self.printer = ColorPrinter()
        self.navigation = NavigationController()
        self.code_viewer = CodeViewer(explorer_config.get('syntax_highlighting', True))
        
        # Exploration state
        self.current_node: Optional[CallTreeNode] = None
        self.call_tree: Optional[CallTree] = None
        self.navigation_stack: List[CallTreeNode] = []
        self.session_data: Dict[str, Any] = {}
        self.start_time: Optional[float] = None
        
        # Statistics
        self.stats = {
            'nodes_visited': 0,
            'code_views': 0,
            'manual_selections': 0,
            'max_depth_reached': 0,
            'exploration_path': [],
            'findings': []
        }
    
    async def initialize_exploration(self, file_path: str, function_name: str, language: str):
        """Initialize a new exploration session."""
        self.session_data = {
            'session_id': f"exploration_{int(time.time())}",
            'file_path': file_path,
            'function_name': function_name,
            'language': language,
            'created_at': datetime.utcnow().isoformat(),
            'last_modified': datetime.utcnow().isoformat()
        }
        
        # Build initial call tree (placeholder implementation)
        await self._build_initial_call_tree(file_path, function_name, language)
        
        log_info(f"Initialized exploration session: {self.session_data['session_id']}")
    
    async def load_session(self, session_name: str):
        """Load a previous exploration session."""
        # Placeholder implementation - would load from storage
        self.printer.print_info(f"Loading session: {session_name}")
        
        # Mock session data
        self.session_data = {
            'session_id': session_name,
            'file_path': 'example.py',
            'function_name': 'main',
            'language': 'python',
            'created_at': datetime.utcnow().isoformat(),
            'last_modified': datetime.utcnow().isoformat()
        }
        
        await self._build_initial_call_tree(
            self.session_data['file_path'],
            self.session_data['function_name'],
            self.session_data['language']
        )
    
    async def start_exploration(self) -> Dict[str, Any]:
        """Start the interactive exploration session."""
        try:
            self.start_time = time.time()
            
            # Show exploration header
            self._show_exploration_header()
            
            # Main exploration loop
            result = await self._exploration_loop()
            
            # Calculate final statistics
            duration = time.time() - self.start_time
            result.update({
                'duration': duration,
                'session_name': self.session_data.get('session_id'),
                'start_time': self.start_time,
                **self.stats
            })
            
            return result
            
        except KeyboardInterrupt:
            self.printer.print_warning("\nExploration interrupted by user")
            return {'cancelled': True, 'reason': 'user_interrupt'}
        except Exception as e:
            self.printer.print_error(f"Exploration error: {e}")
            return {'cancelled': True, 'reason': 'error', 'error': str(e)}
    
    def _show_exploration_header(self):
        """Display exploration session header."""
        self.printer.print_header("Interactive Function Call Tree Exploration", level=1)
        
        session_info = {
            'File': self.session_data.get('file_path', 'unknown'),
            'Function': self.session_data.get('function_name', 'unknown'),
            'Language': self.session_data.get('language', 'unknown'),
            'Session ID': self.session_data.get('session_id', 'unknown')
        }
        
        for key, value in session_info.items():
            self.printer.print_key_value(key, value)
        
        print()
        self.printer.print_info("Use 'help' for available commands, 'quit' to exit")
        self.printer.print_separator()
    
    async def _build_initial_call_tree(self, file_path: str, function_name: str, language: str):
        """Build the initial call tree for exploration."""
        # Placeholder implementation - would use actual MCP servers
        
        # Create root node
        root_node = CallTreeNode(
            name=function_name,
            file_path=file_path,
            line_number=1,
            language=language,
            node_type='function',
            signature=f'{function_name}()',
            documentation=f'Entry point function: {function_name}'
        )
        
        # Add some mock child nodes
        child_nodes = []
        for i, child_name in enumerate(['init_system', 'process_data', 'cleanup'], 1):
            child_node = CallTreeNode(
                name=child_name,
                file_path=file_path,
                line_number=(i + 1) * 10,
                language=language,
                node_type='function',
                signature=f'{child_name}()',
                documentation=f'Function: {child_name}'
            )
            child_node.complexity_score = 3.0 + i
            child_node.short_description = f"Handles {child_name.replace('_', ' ')}"
            child_nodes.append(child_node)
            root_node.add_callee(child_node)
        
        # Create call tree
        self.call_tree = CallTree(root_node)
        self.call_tree.add_node(root_node)
        for child in child_nodes:
            self.call_tree.add_node(child)
        
        self.call_tree.metadata.update({
            'analysis_type': 'interactive',
            'language': language,
            'total_nodes': len(child_nodes) + 1,
            'max_depth': 2
        })
        
        # Set current node
        self.current_node = root_node
        self.navigation_stack = [root_node]
    
    async def _exploration_loop(self) -> Dict[str, Any]:
        """Main exploration loop."""
        while True:
            try:
                # Display current node information
                await self._display_current_node()
                
                # Get candidate nodes for exploration
                candidates = self._get_candidate_nodes()
                
                # Handle user interaction
                action = await self._handle_user_interaction(candidates)
                
                if action['type'] == 'quit':
                    break
                elif action['type'] == 'navigate':
                    await self._handle_navigation(action['target'])
                elif action['type'] == 'select_nodes':
                    await self._handle_node_selection(action['nodes'])
                elif action['type'] == 'back':
                    await self._handle_back_navigation()
                elif action['type'] == 'view_code':
                    await self._handle_code_viewing()
                elif action['type'] == 'help':
                    self._show_help()
                elif action['type'] == 'session_info':
                    self._show_session_info()
                
            except KeyboardInterrupt:
                break
            except Exception as e:
                self.printer.print_error(f"Error in exploration loop: {e}")
                continue
        
        return {'completed': True}
    
    async def _display_current_node(self):
        """Display information about the current node."""
        if not self.current_node:
            self.printer.print_error("No current node available")
            return
        
        print()
        self.printer.print_separator("=", 60)
        self.printer.print_colored(f"Current Node: {self.current_node.name}", Color.BRIGHT_WHITE)
        
        # Node details
        details = {
            'File': f"{self.current_node.file_path}:{self.current_node.line_number}",
            'Language': self.current_node.language,
            'Type': self.current_node.node_type
        }
        
        if self.current_node.signature:
            details['Signature'] = self.current_node.signature
        
        if hasattr(self.current_node, 'short_description') and self.current_node.short_description:
            details['Description'] = self.current_node.short_description
        elif self.current_node.documentation:
            desc = self.current_node.documentation[:100]
            if len(self.current_node.documentation) > 100:
                desc += "..."
            details['Description'] = desc
        
        if self.explorer_config.get('show_complexity') and hasattr(self.current_node, 'complexity_score'):
            details['Complexity'] = f"{self.current_node.complexity_score:.1f}/10"
        
        for key, value in details.items():
            self.printer.print_key_value(key, str(value))
        
        self.printer.print_separator("=", 60)
        
        # Update statistics
        self.stats['nodes_visited'] += 1
        self.stats['max_depth_reached'] = max(self.stats['max_depth_reached'], len(self.navigation_stack))
        
        # Add to exploration path
        node_info = {
            'name': self.current_node.name,
            'file_path': self.current_node.file_path,
            'line_number': self.current_node.line_number,
            'timestamp': time.time()
        }
        self.stats['exploration_path'].append(node_info)
    
    def _get_candidate_nodes(self) -> List[CallTreeNode]:
        """Get candidate nodes for exploration from current node."""
        if not self.current_node:
            return []
        
        return self.current_node.callee_nodes
    
    async def _handle_user_interaction(self, candidates: List[CallTreeNode]) -> Dict[str, Any]:
        """Handle user interaction and return action."""
        # Ask if user wants to read code
        read_code = await self._prompt_yes_no("Do you want to read the code of this node?", default=False)
        
        if read_code:
            await self._handle_code_viewing()
            
            # Show navigation options
            print()
            self.printer.print_info("Navigation options:")
            self.printer.print_info("  [Enter] - Continue to select subnodes")
            self.printer.print_info("  [Return/back] - Go back to parent node")
            
            nav_choice = await self._prompt_input("Press Enter to continue, 'back' to go back, or command")
            
            if nav_choice.lower() in ['back', 'return', 'b']:
                return {'type': 'back'}
        
        # Display available subnodes
        if not candidates:
            self.printer.print_warning("No subnodes available")
            return await self._handle_leaf_node()
        
        return await self._display_node_selection(candidates)
    
    async def _display_node_selection(self, candidates: List[CallTreeNode]) -> Dict[str, Any]:
        """Display candidate nodes and handle selection."""
        print()
        self.printer.print_colored(f"Available subnodes for {self.current_node.name}:", Color.BRIGHT_YELLOW)
        self.printer.print_separator("─", 40)
        
        # Display numbered candidates
        for i, node in enumerate(candidates):
            complexity_str = ""
            if self.explorer_config.get('show_complexity') and hasattr(node, 'complexity_score'):
                complexity_str = f" | Complexity: {node.complexity_score:.1f}"
            
            self.printer.print_colored(f"  {i}: {node.name}", Color.WHITE)
            self.printer.print_colored(f"     Type: {node.node_type}{complexity_str}", Color.BRIGHT_BLACK)
            
            if hasattr(node, 'short_description') and node.short_description:
                desc = node.short_description[:80]
                if len(node.short_description) > 80:
                    desc += "..."
                self.printer.print_colored(f"     Desc: {desc}", Color.BRIGHT_BLACK)
            
            print()
        
        # Show selection options
        self.printer.print_info("Selection options:")
        self.printer.print_info("  • Single: Enter node number (e.g., '2')")
        self.printer.print_info("  • Multiple: Enter weighted format (e.g., '1,0.5;2,0.1;4,0.4')")
        self.printer.print_info("  • Commands: 'back', 'help', 'quit', 'info <number>'")
        
        while True:
            user_input = await self._prompt_input(f"Your selection for {self.current_node.name}")
            
            # Handle commands
            if user_input.lower() == 'back':
                return {'type': 'back'}
            elif user_input.lower() == 'quit':
                return {'type': 'quit'}
            elif user_input.lower() == 'help':
                return {'type': 'help'}
            elif user_input.lower().startswith('info '):
                try:
                    idx = int(user_input.split()[1])
                    if 0 <= idx < len(candidates):
                        self._display_detailed_node_info(candidates[idx])
                        continue
                    else:
                        self.printer.print_error("Invalid node number")
                        continue
                except (IndexError, ValueError):
                    self.printer.print_error("Invalid format. Use 'info <number>'")
                    continue
            
            # Parse selection
            try:
                selection = self._parse_selection(user_input, candidates)
                self.stats['manual_selections'] += 1
                return {'type': 'select_nodes', 'nodes': selection}
            except Exception as e:
                self.printer.print_error(f"Invalid selection: {e}")
                continue
    
    def _parse_selection(self, user_input: str, candidates: List[CallTreeNode]) -> List[Tuple[CallTreeNode, float]]:
        """Parse user selection input."""
        user_input = user_input.strip()
        
        if ',' in user_input and ';' in user_input:
            # Multiple weighted selection: "1,0.5;2,0.1;4,0.4"
            selections = []
            total_weight = 0
            
            for pair in user_input.split(';'):
                try:
                    idx_str, weight_str = pair.split(',')
                    idx = int(idx_str.strip())
                    weight = float(weight_str.strip())
                    
                    if idx < 0 or idx >= len(candidates):
                        raise ValueError(f"Node index {idx} out of range (0-{len(candidates)-1})")
                    
                    if weight < 0 or weight > 1:
                        raise ValueError(f"Weight {weight} must be between 0 and 1")
                    
                    selections.append((candidates[idx], weight))
                    total_weight += weight
                    
                except ValueError as e:
                    raise ValueError(f"Invalid pair '{pair}': {e}")
            
            # Validate total weight
            if abs(total_weight - 1.0) > 0.001:
                self.printer.print_warning(f"Total weight {total_weight:.3f} != 1.0, normalizing...")
                # Normalize weights
                selections = [(node, weight/total_weight) for node, weight in selections]
            
            return selections
        else:
            # Single selection
            try:
                idx = int(user_input)
                if idx < 0 or idx >= len(candidates):
                    raise ValueError(f"Node index {idx} out of range (0-{len(candidates)-1})")
                return [(candidates[idx], 1.0)]
            except ValueError:
                raise ValueError("Single selection must be a valid node number")
    
    def _display_detailed_node_info(self, node: CallTreeNode):
        """Display detailed information about a specific node."""
        print()
        self.printer.print_separator("=", 50)
        self.printer.print_colored(f"Detailed Info: {node.name}", Color.BRIGHT_CYAN)
        self.printer.print_separator("=", 50)
        
        details = {
            'File': node.file_path,
            'Line': node.line_number,
            'Language': node.language,
            'Type': node.node_type
        }
        
        if node.signature:
            details['Signature'] = node.signature
        
        if node.documentation:
            details['Documentation'] = node.documentation
        
        if hasattr(node, 'complexity_score'):
            details['Complexity'] = f"{node.complexity_score:.1f}/10"
        
        if hasattr(node, 'semantic_tags') and node.semantic_tags:
            details['Tags'] = ', '.join(node.semantic_tags)
        
        if hasattr(node, 'call_frequency'):
            details['Call Frequency'] = node.call_frequency
        
        for key, value in details.items():
            self.printer.print_key_value(key, str(value))
        
        self.printer.print_separator("=", 50)
    
    async def _handle_node_selection(self, selected_nodes: List[Tuple[CallTreeNode, float]]):
        """Handle the selection of one or more nodes."""
        if len(selected_nodes) == 1:
            # Single node - navigate directly
            node, weight = selected_nodes[0]
            self.navigation_stack.append(node)
            self.current_node = node
            self.printer.print_success(f"→ Navigating to: {node.name}")
        else:
            # Multiple weighted nodes - show exploration options
            self.printer.print_info(f"Exploring {len(selected_nodes)} nodes with weights:")
            for node, weight in selected_nodes:
                self.printer.print_info(f"  • {node.name}: {weight:.2f}")
            
            # For now, navigate to the highest weighted node
            # In a full implementation, this could trigger MCTS exploration
            best_node, best_weight = max(selected_nodes, key=lambda x: x[1])
            self.navigation_stack.append(best_node)
            self.current_node = best_node
            self.printer.print_success(f"→ Navigating to highest weighted: {best_node.name} ({best_weight:.2f})")
    
    async def _handle_back_navigation(self):
        """Handle navigation back to parent node."""
        if len(self.navigation_stack) > 1:
            self.navigation_stack.pop()
            self.current_node = self.navigation_stack[-1]
            self.printer.print_info(f"← Returning to: {self.current_node.name}")
        else:
            self.printer.print_warning("Already at root node")
    
    async def _handle_code_viewing(self):
        """Handle code viewing for current node."""
        if not self.current_node:
            self.printer.print_error("No current node to view")
            return
        
        await self.code_viewer.display_node_code(self.current_node)
        self.stats['code_views'] += 1
    
    async def _handle_leaf_node(self) -> Dict[str, Any]:
        """Handle when user reaches a leaf node."""
        self.printer.print_info(f"Leaf node reached: {self.current_node.name}")
        
        print()
        self.printer.print_info("Options:")
        self.printer.print_info("  [1] Go back to parent")
        self.printer.print_info("  [2] Start new exploration from root")
        self.printer.print_info("  [3] Exit exploration")
        
        choice = await self._prompt_input("Your choice")
        
        if choice == "1" and len(self.navigation_stack) > 1:
            return {'type': 'back'}
        elif choice == "2":
            # Reset to root
            self.navigation_stack = [self.navigation_stack[0]]
            self.current_node = self.navigation_stack[0]
            return {'type': 'navigate', 'target': 'root'}
        else:
            return {'type': 'quit'}
    
    def _show_help(self):
        """Display help information."""
        help_text = """
Available Commands:
  
Node Selection:
  <number>              - Select single node by number
  1,0.5;2,0.3;3,0.2    - Select multiple nodes with weights
  
Navigation:
  back                  - Go back to parent node
  info <number>         - Show detailed info about a node
  
Session:
  help                  - Show this help
  quit                  - Exit exploration
  
During code viewing:
  Enter                 - Continue to node selection
  back/return          - Go back to parent node

Examples:
  2                     - Navigate to node 2
  0,0.6;1,0.4          - Explore nodes 0 and 1 with weights
  info 3               - Show details about node 3
        """
        
        self.printer.print_header("Help", level=2)
        print(help_text.strip())
    
    def _show_session_info(self):
        """Display session information."""
        self.printer.print_header("Session Information", level=2)
        
        info = {
            'Session ID': self.session_data.get('session_id', 'unknown'),
            'Duration': f"{time.time() - self.start_time:.2f}s" if self.start_time else 'unknown',
            'Nodes Visited': self.stats['nodes_visited'],
            'Current Depth': len(self.navigation_stack),
            'Max Depth Reached': self.stats['max_depth_reached'],
            'Code Views': self.stats['code_views'],
            'Manual Selections': self.stats['manual_selections']
        }
        
        for key, value in info.items():
            self.printer.print_key_value(key, str(value))
    
    async def _prompt_yes_no(self, message: str, default: bool = False) -> bool:
        """Prompt user for yes/no input."""
        default_str = "y/N" if not default else "Y/n"
        response = await self._prompt_input(f"{message} ({default_str})")
        
        if not response:
            return default
        
        return response.lower() in ['y', 'yes', 'true', '1']
    
    async def _prompt_input(self, message: str) -> str:
        """Prompt user for input with timeout."""
        timeout = self.explorer_config.get('timeout_seconds', 300)
        
        try:
            # Simple input for now - in full implementation would use async input with timeout
            return input(f"{message}: ").strip()
        except EOFError:
            raise KeyboardInterrupt("Input interrupted")